// Tencent is pleased to support the open source community by making tRPC available.
//
// Copyright (C) 2023 THL A29 Limited, a Tencent company.
// All rights reserved.
//
// If you have downloaded a copy of the tRPC source code from Tencent,
// please note that tRPC source code is licensed under the  Apache 2.0 License,
// A copy of the Apache 2.0 License is included in this file.

// Package lang encapsulates language-related functionality.
package lang

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"unicode"

	"github.com/iancoleman/strcase"

	"trpc.group/trpc-go/trpc-cmdline/descriptor"
)

// PBSimplifyGoType determine whether to use fullyQualifiedPackageName or not,
// if the `fullTypeName` occur in code of `package goPackageName`, `package` part
// should be removed.
func PBSimplifyGoType(fullTypeName string, goPackageName string) string {
	idx := strings.LastIndex(fullTypeName, ".")
	if idx <= 0 {
		panic(fmt.Sprintf("invalid fullyQualifiedType: %s", fullTypeName))
	}

	pkg := fullTypeName[0:idx]
	typ := fullTypeName[idx+1:]

	if pkg == goPackageName {
		return typ
	}
	return fullTypeName
}

// PBGoType convert `t` to go style (like a.b.c.hello, it'll be changed to a_b_c.Hello)
func PBGoType(t string) string {
	var prefix string

	idx := strings.LastIndex(t, "/")
	if idx >= 0 {
		prefix = t[:idx]
		t = t[idx+1:]
	}

	idx = strings.LastIndex(t, ".")
	if idx <= 0 {
		panic(fmt.Sprintf("invalid go type: %s", t))
	}

	gopkg := PBGoPackage(t[0:idx])
	msg := t[idx+1:]

	return GoExport(prefix + gopkg + "." + msg)
}

// PBGoPackage convert a.b.c to a_b_c
//
// if there's following option `go_package="trpc.group/hello;world"`,
// then PBGoPackage should return `world` instead of `hello`.
func PBGoPackage(pkgName string) string {
	var (
		prefix string
		pkg    string
	)
	idx := strings.LastIndex(pkgName, "/")
	if idx < 0 {
		pkg = pkgName
	} else {
		prefix = pkgName[0:idx]
		pkg = pkgName[idx+1:]
	}

	pkg = strings.Replace(pkg, "-", "_", -1)
	gopkg := strings.Replace(pkg, ".", "_", -1)
	_ = prefix

	return TrimLeft(";", gopkg)
}

// GoExport export go type
func GoExport(typ string) string {
	idx := strings.LastIndex(typ, ".")
	if idx < 0 {
		return strings.Title(typ)
	}
	return typ[0:idx] + "." + strings.Title(typ[idx+1:])
}

// SplitList split string `str` via delimiter `sep` into a list of string
func SplitList(sep, str string) []string {
	return strings.Split(str, sep)
}

// ReverseList reverse the order from end to start for a list.
func ReverseList(s []string) []string {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
	return s
}

// TrimRight trim right substr starting at `sep`
func TrimRight(sep, str string) string {
	idx := strings.LastIndex(str, sep)
	if idx < 0 {
		return str
	}
	return str[:idx]
}

// TrimLeft trim left substr starting at `sep`
func TrimLeft(sep, str string) string {
	idx := strings.Index(str, sep)
	if idx < 0 {
		return str
	}
	return str[idx+len(sep):]
}

// Title uppercase the first character of `s`
func Title(s string) string {
	for k, v := range s {
		return string(unicode.ToUpper(v)) + s[k+1:]
	}
	return ""
}

// UnTitle make the first character of s lowercase
func UnTitle(s string) string {
	for k, v := range s {
		return string(unicode.ToLower(v)) + s[k+1:]
	}
	return ""
}

// GoFullyQualifiedType convert $repo/$pkg.$type to $realpkg.$type, where $realpkg is calculated
// by `package directive` and `go_package` file option.
//
// For example:
// ```proto
// option go_package="github.com/abc/abc;def"
// message XXXX{}
// ```
// In this example:
// - importPath should be `import def "github.com/abc/abc"`
// - typeName should be `def.XXXX` rather than `abc.XXXX`.
func GoFullyQualifiedType(pbFullyQualifiedType string, nfd *descriptor.FileDescriptor) string {
	return goFullyQualifiedTypeCommon(pbFullyQualifiedType, nfd, goFullyQualifiedType)
}

// GoFullyQualifiedTypeX is for replacing GoFullyQualifiedType.
func GoFullyQualifiedTypeX(pbFullyQualifiedType string, nfd *descriptor.FileDescriptor) string {
	return goFullyQualifiedTypeCommon(pbFullyQualifiedType, nfd, goFullyQualifiedTypeX)
}

func goFullyQualifiedTypeCommon(
	pbFullyQualifiedType string,
	nfd *descriptor.FileDescriptor,
	rpcMessageTypeHandle func(pb, typ string, nfd *descriptor.FileDescriptor) string,
) string {
	idx := strings.LastIndex(pbFullyQualifiedType, "/")
	fulltyp := pbFullyQualifiedType[idx+1:] // $pkgdirective.$typename

	// Replace package name of RequestType/ResponseType.
	idx = strings.LastIndex(fulltyp, ".")
	if idx <= 0 {
		panic(fmt.Errorf("invalid type:%s", fulltyp))
	}

	typ := Camelcase(fulltyp[idx+1:])

	// In case there are multiple pb files with the same package directive but different go_package,
	// the mapping from package directive to go_package in nfd.
	// Pkg2ValidGoPkg is incorrect and needs to be specific to the file name.
	// However, the RequestType generated by jhump/protoreflect is in the form of ${package-directive}.
	// RequestType without specifying the information about which pb file RequestType is defined in.
	//
	// The following approach is needed to locate the specific pb file where the message is defined:
	// - Traverse all the messages and their definitions to find the pb file that matches the message name completely,
	//   and find the corresponding package for that pb file.
	// - Rewrite the complete type of the request body.
	rtype := pbFullyQualifiedType
	if len(nfd.RPCMessageType) != 0 {
		// Found the pb file where the message is defined.
		pb, ok := nfd.RPCMessageType[pbFullyQualifiedType]
		if !ok || len(pb) == 0 {
			panic(fmt.Errorf("cannot find the protofile containing definition of %s", pbFullyQualifiedType))
		}
		return rpcMessageTypeHandle(pb, typ, nfd)
	}
	return rtype
}

func goFullyQualifiedType(pb, typ string, nfd *descriptor.FileDescriptor) string {
	// Find the corresponding concrete type name based on the pb file.
	validGoPkg, ok := nfd.Pb2ValidGoPkg[pb]
	if !ok {
		panic(fmt.Errorf("get valid gopkg of %s fail", pb))
	}
	return validGoPkg + "." + typ
}

func goFullyQualifiedTypeX(pb, typ string, nfd *descriptor.FileDescriptor) string {
	// Find the corresponding concrete type name based on the pb file.
	importPath, ok := nfd.Pb2ImportPath[pb]
	if !ok {
		panic(fmt.Errorf("get importPath of %s fail", pb))
	}
	_, path := ExplodeImport(importPath)
	// Defined and defined in dependent files.
	for _, v := range nfd.ImportsX {
		if v.Path != path {
			continue
		}
		// rtype = v.Path + "." + typ
		return v.Name + "." + typ
	}
	// Defined and defined in --protofile.
	return nfd.BaseGoPackageName + "." + typ
}

// ExplodeImport splits an import path into the importName and importPath.
func ExplodeImport(s string) (importName, importPath string) {
	idx := strings.LastIndex(s, ";")
	if idx != -1 {
		importName = s[idx+1:]
		importPath = s[:idx]
		return PBGoPackage(importName), importPath
	}
	idx = strings.LastIndex(s, "/")
	if idx != -1 {
		importName = s[idx+1:]
		importPath = s
		return PBGoPackage(importName), importPath
	}
	return PBGoPackage(s), s
}

// PBValidGoPackage returns a valid go package.
//
// Consider the scenario where option go_package="trpc.group/hello;world" is present. In this case,
// the importPath should be trpc.group/hello, but the importName should be world.
//
// In other words, referencing a type with hello.XXX is incorrect and world.XXX should be used instead.
func PBValidGoPackage(pkgName string) string {
	var (
		pkg string
	)
	idx := strings.LastIndex(pkgName, "/")
	if idx < 0 {
		pkg = pkgName
	} else {
		pkg = pkgName[idx+1:]
	}

	pkg = strings.Replace(pkg, "-", "_", -1)
	pkg = strings.Replace(pkg, ".", "_", -1)

	return TrimLeft(";", pkg)
}

// Last returns the last element in `list`
func Last(list []string) string {
	idx := len(list) - 1
	return list[idx]
}

// HasPrefix test whether string `str` has prefix `prefix`
func HasPrefix(prefix, str string) bool {
	return strings.HasPrefix(str, prefix)
}

// HasSuffix test whether string `str` has suffix `suffix`
func HasSuffix(suffix, str string) bool {
	return strings.HasSuffix(str, suffix)
}

// Add adds two number.
func Add(num1, num2 int) int {
	return num1 + num2
}

// LoadGoMod load the module name of current directory.
func LoadGoMod() (string, error) {
	fin, err := openGoModFile()
	if err != nil {
		return "", err
	}
	sc := bufio.NewScanner(fin)
	for sc.Scan() {
		l := sc.Text()
		if strings.HasPrefix(l, "module ") {
			return strings.Split(l, " ")[1], nil
		}
	}
	return "", nil
}

func openGoModFile() (*os.File, error) {
	d, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	p := filepath.Join(d, "go.mod")
	_, err = os.Lstat(p)
	if err != nil {
		return nil, err
	}

	return os.Open(p)
}

// CheckSECVTpl checks whether the Validation feature is enabled to determine the exported template content.
func CheckSECVTpl(pkgMap map[string]string) bool {
	if _, isKeyFound := pkgMap["validate"]; isKeyFound {
		return true
	}
	if _, isKeyFound := pkgMap["trpc.v2.validate"]; isKeyFound {
		return true
	}
	return false
}

// Camelcase converts a string into camel case.
// Special cases must be compatible with existing protocols, otherwise they are converted to camel case naming.
func Camelcase(s string) string {
	// Changes in camel case for existing protocols are not backward compatible.
	// No changes are made to all uppercase letters.

	// The template expects message names to be in all capital letters and underscores,
	// so we need to keep the original text.
	if len(s) == 0 {
		return s
	}

	wordList := strings.Split(s, "_")
	if len(wordList) == 1 {
		if isAllUpper(wordList[0]) {
			return s
		}
		return strcase.ToCamel(s)
	}

	return CamelcaseList(wordList)
}

// CamelcaseList converts a list of strings to camel case and returns the last string.
func CamelcaseList(wordList []string) string {
	var camelWord string
	for i := 0; i < len(wordList); i++ {
		cur, seq := camelcaseListItem(wordList, i)
		camelWord = fmt.Sprintf("%s%s%s", camelWord, cur, seq)
	}

	return camelWord
}

func camelcaseListItem(wordList []string, i int) (string, string) {
	cur := wordList[i]
	seq := getCamelcaseSeq(wordList, i, cur)

	if !isAllUpper(cur) {
		cur = strcase.ToCamel(cur)
	}
	return cur, seq
}

func getCamelcaseSeq(wordList []string, i int, cur string) string {
	seq := ""
	// If the current and the next word are both uppercase, they should be joined with an underscore.
	if i != len(wordList)-1 && len(wordList[i+1]) != 0 {
		curIsAllUpper := isAllUpper(cur)
		nextIsAllUpper := isAllUpper(wordList[i+1])
		if curIsAllUpper || nextIsAllUpper ||
			// Only skip '_' when there is a lower case letter behind.
			// That is to say:
			// If the '_' is followed by an upper case letter, the '_' should be preserved.
			// https://github.com/protocolbuffers/protobuf-go/blob/v1.26.0/internal/strs/strings.go#L30
			unicode.IsUpper(rune(wordList[i+1][0])) {
			seq = "_"
		}
	}
	return seq
}

// isAllUpper returns whether all the characters are uppercased or not.
func isAllUpper(s string) bool {
	for _, c := range s {
		if !unicode.IsUpper(c) {
			return false
		}
	}
	return true
}

// Concat concats strings.
func Concat(sep string, s ...string) string {
	ss := []string{}
	ss = append(ss, s...)
	return strings.Join(ss, sep)
}

// MergeRPC combines RPCs and outputs the list.
func MergeRPC(v1 []*descriptor.RPCDescriptor, v2 []*descriptor.RPCDescriptor) []*descriptor.RPCDescriptor {
	var result []*descriptor.RPCDescriptor
	result = append(result, v1...)
	result = append(result, v2...)
	return result
}
